/*
 * Copyright © 2013-2014.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.linsoho.xmlupdate;

//import java.beans.BeanInfo;
//import java.beans.Introspector;
//import java.beans.PropertyDescriptor;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <pre>
 * http://en.wikipedia.org/wiki/Globally_unique_identifier
 * http://zh.wikipedia.org/wiki/Base64
 * https://github.com/RestExpress/RepoExpress/blob/master/common/src/java/com/strategicgains/repoexpress/util/UuidConverter.java
 * </pre>
 * 
 * @author leo.tu.taipei@gmail.com, leo@ratepo.com
 */
public class UuidUtil {
	static final protected Logger log = LoggerFactory.getLogger(UuidUtil.class);

	static private boolean useJdkBase64 = true;

	/**
	 * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and / changed to - and _ to make the encoded Base64 results more URL-SAFE. This
	 * table is only used when the Base64's mode is set to URL-SAFE.
	 */
	private static final char[] URL_SAFE_ENCODE_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray();

	private static final int[] URL_SAFE_ENCODE_TABLE_I256 = new int[256];
	static {
		for (int i = 0; i < URL_SAFE_ENCODE_TABLE.length; i++) {
			URL_SAFE_ENCODE_TABLE_I256[URL_SAFE_ENCODE_TABLE[i]] = i;
		}
	}

	protected UuidUtil() {
	}

	/**
	 * <pre>
	 * Example: "405af65a-9724-495b-8fc6-f099b701b289" (36 characters) --> "405af65a9724495b8fc6f099b701b289" (32 characters, OLD code)
	 *          --> "QFr2WpckSVuPxvCZtwGyiQ" (22 characters, NEW code)
	 * P.S 128-bit UUID: "405af65a9724495b8fc6f099b701b289" and "QFr2WpckSVuPxvCZtwGyiQ" are the same value.
	 * </pre>
	 * 
	 * @return 22 characters uuid
	 */
	static public String generateUUID() {
		return generateBase64UUID();
	}

	static public String generateUUID(UUID uuid) {
		return generateBase64UUID(uuid);
	}

	/**
	 * @return 22
	 */
	static public int generateUUIDLength() {
		return generateBase64UUIDLength();
	}

	/**
	 * Example: "QFr2WpckSVuPxvCZtwGyiQ" (22 characters)
	 * 
	 * @return URL-safe base encoding scheme uuid (e.g. no "==" on the end, without '+/' characters)
	 */
	static public String generateBase64UUID() {
		return encodeBase64(toBytes(UUID.randomUUID()));
	}

	static public String generateBase64UUID(UUID uuid) {
		return encodeBase64(toBytes(uuid));
	}
	
	/**
	 * 
	 * @return byte[16]
	 */
	static public byte[] toBytes(UUID uuid) {
		ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
		buffer.putLong(uuid.getMostSignificantBits());
		buffer.putLong(uuid.getLeastSignificantBits());
		return buffer.array();
	}

	/**
	 * Example: "QFr2WpckSVuPxvCZtwGyiQ" (22 characters)
	 * 
	 * @return 22 characters uuid
	 */
	static public String generate22UUID() {
		return generateUUID();
	}

	static public String generate22UUID(UUID uuid) {
		return generateUUID(uuid);
	}

	/**
	 * Example: "6de59f857c9644609b2553b73463ba67" (32 characters)
	 * 
	 * @return 32 characters uuid
	 */
	static public String generate32UUID() {
		String str = generate36UUID();
		int len = str.length();
		StringBuilder sb = new StringBuilder(len - 4);
		for (int i = 0; i < len; i++) {
			if (str.charAt(i) != '-') {
				sb.append(str.charAt(i));
			}
		}
		return sb.toString();
	}

	static public String generate32UUID(UUID uuid) {
		String str = generate36UUID(uuid);
		int len = str.length();
		StringBuilder sb = new StringBuilder(len - 4);
		for (int i = 0; i < len; i++) {
			if (str.charAt(i) != '-') {
				sb.append(str.charAt(i));
			}
		}
		return sb.toString();
	}

	/**
	 * Example: "6de59f85-7c96-4460-9b25-53b73463ba67" (36 characters)
	 * 
	 * @return 36 characters uuid
	 */
	static public String generate36UUID() {
		return UUID.randomUUID().toString();
	}

	static public String generate36UUID(UUID uuid) {
		return uuid.toString();
	}

	/**
	 * @return 22
	 */
	static public int generateBase64UUIDLength() {
		return 22;
	}

	/**
	 * uuid32 to uuid22
	 */
	static public String toBase64UUID(String uuid32) {
		if (uuid32.length() != 32) {
			throw new IllegalArgumentException("(uuid32.length() != 32), uuid32=" + uuid32);
		}
		// 38c14ec3-1078-4278-b705-efe8f4e47e99
		String uuid36 = uuid32.substring(0, 8) + "-" + uuid32.substring(8, 12) + "-" + uuid32.substring(12, 16) + "-" + uuid32.substring(16, 20) + "-"
				+ uuid32.substring(20);
		// log.debug("uuid36=" + uuid36 + ", uuid32=" + uuid32);
		return encodeBase64(toBytes(UUID.fromString(uuid36)));
	}

	static public boolean isBase64UUIDChar(String str) {
		if (str == null || str.isEmpty() || str.length() != generateBase64UUIDLength()) {
			return false;
		}
		int strLen = str.length();
		for (int i = 0; i < strLen; i++) {
			char ch = str.charAt(i);
			// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
			// XXX: http://www.ascii-code.com/, http://www.asciitable.com/
			if (ch >= '0' && ch <= '9') {
				continue;
			}
			if (ch >= 'A' && ch <= 'Z') {
				continue;
			}
			if (ch >= 'a' && ch <= 'z') {
				continue;
			}
			if (ch == '-' || ch == '_') {
				continue;
			} else {
				// log.debug("invalid ch=" + ch);
				return false;
			}
		}
		return true;
	}

	/**
	 * Given a UUID representation (either a short or long form), return a UUID from it.
	 * <p/>
	 * If the uuidString is longer than our short, 22-character form (or 24 with padding), it is assumed to be a full-length 36-character UUID string.
	 * 
	 * @param uuidString
	 *            a string representation of a UUID.
	 * @return a UUID instance
	 * @throws IllegalArgumentException
	 *             if the uuidString is not a valid UUID representation.
	 * @throws NullPointerException
	 *             if the uuidString is null.
	 */
	public static UUID parse(String uuid22OrUUid36) {
		if (uuid22OrUUid36 == null) {
			throw new NullPointerException("Null UUID string");
		}
		if (uuid22OrUUid36.length() > 24) { // uuid 36
			return UUID.fromString(uuid22OrUUid36);
		}

		if (uuid22OrUUid36.length() < generateBase64UUIDLength()) {
			throw new IllegalArgumentException("Short UUID must be " + generateBase64UUIDLength() + " characters: " + uuid22OrUUid36);
		}
		return fromBytes(decodeBase64(uuid22OrUUid36)); // uuid 22
	}
	
	public static UUID fromBytes(byte[] bytes) {
		ByteBuffer buffer = ByteBuffer.wrap(new byte[16]);
		buffer.put(bytes, 0, 16);
		buffer.clear();
		return new UUID(buffer.getLong(), buffer.getLong());
	}

	/**
	 * Accepts a UUID byte array (of exactly 16 bytes) and base64 encodes it, using a URL-safe encoding scheme. The resulting string will be 22
	 * characters in length with no extra padding on the end (e.g. no "==" on the end).
	 * <p/>
	 * Base64 encoding essentially takes each three bytes from the array and converts them into four characters. This implementation, not using
	 * padding, converts the last byte into two characters.
	 * 
	 * @param bytes
	 *            a UUID byte array.
	 * @return a URL-safe base64-encoded string.
	 */
	public static String encodeBase64(byte[] bytes) {
		if (bytes == null) {
			throw new NullPointerException("Null UUID byte array");
		}
		if (bytes.length != 16) {
			throw new IllegalArgumentException("UUID must be 16 bytes");
		}
		if (useJdkBase64) {
			byte[] encodeData = Base64.getUrlEncoder().encode(bytes); // RFC4648_URLSAFE
			if (encodeData[encodeData.length - 2] == '=' && encodeData[encodeData.length - 1] == '=') {
				// remove "=="
				return new String(encodeData, 0, encodeData.length - 2, StandardCharsets.ISO_8859_1);
			} else {
				throw new IllegalArgumentException("!(encodeData[encodeData.length - 2] == '=' && encodeData[encodeData.length - 1] == '=')");
				// return new String(encodeData, StandardCharsets.ISO_8859_1);
			}
		}

		char[] encodeTable = URL_SAFE_ENCODE_TABLE;
		// Output is always 22 characters.
		char[] chars = new char[22];

		int i = 0;
		int j = 0;

		while (i < 15) {
			// Get the next three bytes.
			int d = (bytes[i++] & 0xff) << 16 | (bytes[i++] & 0xff) << 8 | (bytes[i++] & 0xff);

			// Put them in these four characters
			chars[j++] = encodeTable[(d >>> 18) & 0x3f];
			chars[j++] = encodeTable[(d >>> 12) & 0x3f];
			chars[j++] = encodeTable[(d >>> 6) & 0x3f];
			chars[j++] = encodeTable[d & 0x3f];
		}

		// The last byte of the input gets put into two characters at the end of the string.
		int d = (bytes[i] & 0xff) << 10;
		chars[j++] = encodeTable[d >> 12];
		chars[j++] = encodeTable[(d >>> 6) & 0x3f];
		return new String(chars);
	}

	/**
	 * Base64 decodes a short, 22-character UUID string (or 24-characters with padding) into a byte array. The resulting byte array contains 16 bytes.
	 * <p/>
	 * Base64 decoding essentially takes each four characters from the string and converts them into three bytes. This implementation, not using
	 * padding, converts the final two characters into one byte.
	 * 
	 * @param s
	 * @return
	 */
	public static byte[] decodeBase64(String s) {
		if (s == null) {
			throw new NullPointerException("Cannot decode null string");
		}
		if (s.isEmpty() || (s.length() > 24)) {
			throw new IllegalArgumentException("Invalid short UUID");
		}
		if (useJdkBase64) {
			byte[] bytes = Base64.getUrlDecoder().decode(s); // RFC4648_URLSAFE
			return bytes;
		}

		// Output is always 16 bytes (UUID).
		byte[] bytes = new byte[16];
		int i = 0;
		int j = 0;

		while (i < 15) {
			// Get the next four characters.
			int d = URL_SAFE_ENCODE_TABLE_I256[s.charAt(j++)] << 18 | URL_SAFE_ENCODE_TABLE_I256[s.charAt(j++)] << 12
					| URL_SAFE_ENCODE_TABLE_I256[s.charAt(j++)] << 6 | URL_SAFE_ENCODE_TABLE_I256[s.charAt(j++)];

			// Put them in these three bytes.
			bytes[i++] = (byte) (d >> 16);
			bytes[i++] = (byte) (d >> 8);
			bytes[i++] = (byte) d;
		}

		// Add the last two characters from the string into the last byte.
		bytes[i] = (byte) ((URL_SAFE_ENCODE_TABLE_I256[s.charAt(j++)] << 18 | URL_SAFE_ENCODE_TABLE_I256[s.charAt(j++)] << 12) >> 16);
		return bytes;
	}

	/**
	 * 依时间带有顺序的UUID
	 * </p>
	 * Base36(HEX) time value
	 * <p/>
	 * Example: "ifv787l7QB67h9WPQlOhpWj4h9P43w" (30 characters) start with ifv787l7
	 * 
	 * @return 30 characters uuid start with serial number
	 */
	static public String generateTime30UUID() {
		return generateTime30UUID(System.currentTimeMillis());
	}

	/**
	 * 依时间带有顺序的UUID
	 * </p>
	 * Base36(HEX) time value
	 * <p/>
	 * Example: "ifv787l7QB67h9WPQlOhpWj4h9P43w" (30 characters) start with ifv787l7
	 * 
	 * @return 30 characters uuid start with serial number
	 */
	static public String generateTime30UUID(Date date) {
		return generateTime30UUID(date.getTime());
	}

	/**
	 * 依时间带有顺序的UUID
	 * </p>
	 * Base36(HEX) time value
	 * <p/>
	 * Example: "ifv787l7QB67h9WPQlOhpWj4h9P43w" (30 characters) start with ifv787l7
	 * 
	 * @return 30 characters uuid start with serial number
	 */
	static public String generateTime30UUID(UUID uuid, Date date) {
		return generateTime30UUID(uuid, date.getTime());
	}
	
	/**
	 * 依时间带有顺序的UUID
	 * </p>
	 * Base36(HEX) time value
	 * <p/>
	 * base36=[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]
	 * <p/>
	 * Example: "ifv787l7QB67h9WPQlOhpWj4h9P43w" (30 characters) start with ifv787l7
	 * 
	 * @param timeValue
	 *            milliseconds
	 * @return 30 characters uuid start with serial number
	 */
	static protected String generateTime30UUID(long timeValue) {
		String value = toBase36HexString(timeValue) + generate22UUID();
		if (value.length() != 30) {
			throw new IllegalArgumentException(
					"(value.length() != 30), value.length=" + value.length() + ", value=" + value + ", timeValue=" + timeValue);
		}
		return value;
	}

	/**
	 * 依时间带有顺序的UUID
	 * </p>
	 * Base36(HEX) time value
	 * <p/>
	 * base36=[0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ]
	 * <p/>
	 * Example: "ifv787l7QB67h9WPQlOhpWj4h9P43w" (30 characters) start with ifv787l7
	 * 
	 * @param uuid
	 *            uuid
	 * @param timeValue
	 *            milliseconds
	 * @return 30 characters uuid start with serial number
	 */
	static protected String generateTime30UUID(UUID uuid, long timeValue) {
		String value = toBase36HexString(timeValue) + generate22UUID(uuid);
		if (value.length() != 30) {
			throw new IllegalArgumentException(
					"(value.length() != 30), value.length=" + value.length() + ", value=" + value + ", timeValue=" + timeValue);
		}
		return value;
	}
	
	/**
	 * 产生同ASCII顺序的Base36Hex
	 * <p/>
	 * 0123456789abcdefghijklmnopqrstuvwxyz (36 radix)
	 * <p/>
	 * https://en.wikipedia.org/wiki/Base36
	 * 
	 * @param timeValue
	 *            milliseconds
	 */
	static private String toBase36HexString(long timeValue) {
		String value = Long.toString(timeValue, Character.MAX_RADIX); // Character.MAX_RADIX=36
		// if (value.charAt(0) == '-') {
		// return value.substring(1);
		// } else {
		return value;
		// }
	}

	public static void main(String args[]) {
		useJdkBase64 = true;
		UUID uuid = UUID.randomUUID();
		log.debug("uuid={}", uuid);
		//
		String uuid22 = generate22UUID(uuid);
		log.debug("useJdkBase64={}, uuid22={}", useJdkBase64, uuid22);

		UUID uuid2 = parse(uuid22);
		log.debug("useJdkBase64={}, uuid2={}", useJdkBase64, uuid2);

		log.debug("useJdkBase64={}, uuid==uuid2: {}", useJdkBase64, uuid.equals(uuid2));

		// ============
		useJdkBase64 = false;

		String uuid22_old = generate22UUID(uuid);
		log.debug("useJdkBase64={}, uuid22={}", useJdkBase64, uuid22_old);

		UUID uuid2_old = parse(uuid22_old);
		log.debug("useJdkBase64={}, uuid2={}", useJdkBase64, uuid2_old);

		log.debug("useJdkBase64={}, uuid==uuid2: {}", useJdkBase64, uuid.equals(uuid2_old));
		//

		log.debug("uuuid22.equals(uuid22_old): {}", uuid22.equals(uuid22_old));
	}
}
